package com.urbanski.util.collections;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * This class represents an object counter, tracking how many of each object exist. Internally, it
 * simply wraps a map of this class's type to Integer, and abstracts the process of initially
 * inserting an object into the map. The following code shows a comparison between using a map
 * directly to count instances and using this class. Assume that collection is a Collection of
 * objects of some type, E.
 * 
 * With a Map:
 * 
 * Map<E, Integer> counts = ...
 * 
 * for (E c : collection) { if (counts.containsKey(c)) { counts.put(c, counts.get(c) + 1); } else {
 * counts.put(c, 1); } }
 * 
 * With this class:
 * 
 * CountingSet<E> counts = ...
 * 
 * for (E c : collection) { counts.increment(c); }
 * 
 * The class also provides convenient constructors that perform this initial counting, simplifying
 * the above code to
 * 
 * CountingSet<E> counts = new CountingSet<E>(collection, true);
 * 
 * @author Mike Urbanski
 * 
 * @param <E>
 *            the type of the objects to count
 */
public class CountingSet<E>
{
	public static final int DOES_NOT_EXIST = Integer.MIN_VALUE;

	private Map<E, Integer> map;

	/**
	 * Constructs a new, empty instance
	 */
	public CountingSet()
	{
		map = new HashMap<E, Integer>();
	}

	/**
	 * Constructs a new instance and populates the initial counts based off the specified array.
	 * This constructor is equivalent to calling CountingSet(objects, true). Initial counts will
	 * be set to 1; each duplicate value in the array will increase the count. For example, if the
	 * array contains {A, B, A, C}, then this CountingSet will contain [A=2, B=1, C=1].
	 * 
	 * @param objects
	 *            the array with which to populate the initial counts
	 */
	public CountingSet(E[] objects)
	{
		this(objects, true);
	}

	/**
	 * Constructs a new instance and populates the initial counts based off the specified array.
	 * Calling this constructor with a boolean value of true is equivalent to calling
	 * CountingSet(objects). If count is true, initial counts will be set to 1, and each duplicate
	 * value in the array will increase the count. For example, if the array contains {A, B, A, C}
	 * and count is true, then this CountingSet will contain [A=2, B=1, C=1]. If count is false,
	 * then each duplicate is ignored. With the same array as above, if count is false, this
	 * CountingSet will contain [A=1, B=1, C=1].
	 * 
	 * @param objects
	 *            the array with which to populate the initial counts
	 * @param count
	 *            whether to count duplicate values in the array
	 */
	public CountingSet(E[] objects, boolean countDuplicates)
	{
		this();
		for (E e : objects)
		{
			if (countDuplicates && map.containsKey(e))
			{
				increment(e);
			}
			else
			{
				setCount(e, 1);
			}
		}
	}

	/**
	 * Constructs a new instance and populates the initial counts based off the specified array.
	 * This constructor is equivalent to calling CountingSet(objects, defaultValue, true). Initial
	 * counts will be set to the specified value; each duplicate value in the array will decrease
	 * the count. For example, if the array contains {A, B, A, C} and defaultValue is 5, then this
	 * CountingSet will contain [A=4, B=5, C=5].
	 * 
	 * @param objects
	 *            the array with which to populate the initial counts
	 * @param defaultValue
	 *            the default value to which to set the count for each object in the array
	 */
	public CountingSet(E[] objects, int defaultValue)
	{
		this(objects, defaultValue, true);
	}

	/**
	 * Constructs a new instance and populates the initial counts based off the specified array.
	 * Calling this constructor with a boolean value of true is equivalent to calling
	 * CountingSet(objects, defaultValue). If decrement is true, then the initial counts will be
	 * set to the specified value, each duplicate value in the array will decrease the count. For
	 * example, if the array contains {A, B, A, C}, defaultValue is 5, and decrement is true, then
	 * this CountingSet will contain [A=4, B=5, C=5]. With the same array and default value as
	 * above, if decrement is false, this CountingSet will contain [A=5, B=5, C=5].
	 * 
	 * @param objects
	 *            the array with which to populate the initial counts
	 * @param defaultValue
	 *            the default value to which to set the count for each object in the array
	 * @param decrement
	 *            whether to decrement the count for each duplicate element in the array
	 */
	public CountingSet(E[] objects, int defaultValue, boolean decrement)
	{
		this();

		for (E e : objects)
		{
			if (decrement && map.containsKey(e))
			{
				decrement(e);
			}
			else
			{
				setCount(e, defaultValue);
			}
		}
	}

	/**
	 * Constructs a new instance and populates the initial counts based off the specified
	 * Collection. This constructor is equivalent to calling CountingSet(objects, true). Initial
	 * counts will be set to 1; each duplicate value in the Collection will increase the count. For
	 * example, if the Collection contains {A, B, A, C}, then this CountingSet will contain [A=2,
	 * B=1, C=1].
	 * 
	 * @param objects
	 *            the Collection with which to populate the initial counts
	 * @param defaultValue
	 *            the default value to which to set the count for each object in the array
	 */
	public CountingSet(Collection<? extends E> objects)
	{
		this(objects, 1, true);
	}

	/**
	 * Constructs a new instance and populates the initial counts based off the specified
	 * Collection. Calling this constructor with a boolean value of true is equivalent to calling
	 * CountingSet(objects). If count is true, initial counts will be set to 1, and each duplicate
	 * value in the Collection will increase the count. For example, if the Collection contains {A,
	 * B, A, C} and count is true, then this CountingSet will contain [A=2, B=1, C=1]. If count is
	 * false, then each duplicate is ignored. With the same Collection as above, if count is false,
	 * this CountingSet will contain [A=1, B=1, C=1].
	 * 
	 * @param objects
	 *            the Collection with which to populate the initial counts
	 * @param count
	 *            whether to count duplicate values in the Collection
	 */
	public CountingSet(Collection<? extends E> objects, boolean count)
	{
		this();
		for (E e : objects)
		{
			if (count && map.containsKey(e))
			{
				increment(e);
			}
			else
			{
				setCount(e, 1);
			}
		}
	}

	/**
	 * Constructs a new instance and populates the initial counts based off the specified
	 * Collection. This constructor is equivalent to calling CountingSet(objects, defaultValue,
	 * true). Initial counts will be set to the specified value; each duplicate value in the
	 * Collection will decrease the count. For example, if the Collection contains {A, B, A, C} and
	 * defaultValue is 5, then this CountingSet will contain [A=4, B=5, C=5].
	 * 
	 * @param objects
	 *            the array with which to populate the initial counts
	 * @param defaultValue
	 *            the default value to which to set the count for each object in the Collection
	 */
	public CountingSet(Collection<? extends E> objects, int defaultValue)
	{
		this(objects, defaultValue, true);
	}

	/**
	 * Constructs a new instance and populates the initial counts based off the specified
	 * Collection. Calling this constructor with a boolean value of true is equivalent to calling
	 * CountingSet(objects, defaultValue). If decrement is true, then the initial counts will be
	 * set to the specified value, each duplicate value in the Collection will decrease the count.
	 * For example, if the Collection contains {A, B, A, C}, defaultValue is 5, and decrement is
	 * true, then this CountingSet will contain [A=4, B=5, C=5]. With the same array and default
	 * value as above, if decrement is false, this CountingSet will contain [A=5, B=5, C=5].
	 * 
	 * @param objects
	 *            the Collection with which to populate the initial counts
	 * @param defaultValue
	 *            the default value to which to set the count for each object in the Collection
	 * @param decrement
	 *            whether to decrement the count for each duplicate element in the Collection
	 */
	public CountingSet(Collection<? extends E> objects, int defaultValue, boolean decrement)
	{
		this();

		for (E e : objects)
		{
			if (decrement && map.containsKey(e))
			{
				decrement(e);
			}
			else
			{
				setCount(e, defaultValue);
			}
		}
	}

	/**
	 * Constructs a new instance with the specified objects and values. The mappings from the
	 * specified map are directly inserted into this object's underlying map.
	 * 
	 * @param values
	 *            the object counts from which to initialize this CountingSet
	 */
	public CountingSet(Map<? extends E, Integer> values)
	{
		this();
		map.putAll(values);
	}

	/**
	 * Increments and returns the count of the specified object, or inserts it with a count of 1 if
	 * it does not yet exist.
	 * 
	 * @param e
	 *            the object to increment
	 * @return the new count of the object
	 */
	public int increment(E e)
	{
		if (map.containsKey(e))
		{
			Integer i = map.get(e) + 1;
			map.put(e, i);
			return i;
		}
		else
		{
			map.put(e, 1);
			return 1;
		}
	}

	/**
	 * Decrements and returns the count of the specified object if the count is greater than 0, or
	 * returns DOES_NOT_EXIST if the object does not exist. If the object's count is already 0, the
	 * count will not go negative. Use decrement(E, boolean) to allow the count to go negative. A
	 * call to this method is equivalent to calling decrement(e, false).
	 * 
	 * @param e
	 *            the object to decrement
	 * @return the new count of the object, with a minimum of 0, or DOES_NOT_EXIST if the object is
	 *         not contained
	 */
	public int decrement(E e)
	{
		return decrement(e, false);
	}

	/**
	 * Decrements and returns the count of the specified object, or returns DOES_NOT_EXIST if the
	 * object does not exist. Allows the count to go negative. A call to this method with a boolean
	 * argument of false is equivalent to calling decrement(e).
	 * 
	 * @param e
	 *            the object to decrement
	 * @param goNegative
	 *            whether the count can be decremented below 0
	 * @return the new count of the object, or DOES_NOT_EXIST if the object is not contained
	 */
	public int decrement(E e, boolean goNegative)
	{
		if (map.containsKey(e))
		{
			Integer i = map.get(e) - 1;
			if (i < 0 && !goNegative)
				i = 0;
			map.put(e, i);
			return i;
		}
		else
		{
			return DOES_NOT_EXIST;
		}
	}

	/**
	 * Sets the count of the specified object to the specified value. If this object is not yet
	 * contained, it will be added, and if it is contained, the old value will be overwritten.
	 * 
	 * @param e
	 *            the object for which to set the count
	 * @param count
	 *            the count to set for the specified object
	 */
	public void setCount(E e, int count)
	{
		map.put(e, count);
	}

	/**
	 * Gets the current count of the specified object, or DOES_NOT_EXIST if the object is not
	 * contained.
	 * 
	 * @param e
	 *            the object for which to get the count
	 * @return the current count of the object, or DOES_NOT_EXIST if the object is not contained
	 */
	public int getCount(E e)
	{
		Integer i = map.get(e);
		if (i == null)
		{
			return DOES_NOT_EXIST;
		}
		else
		{
			return i;
		}
	}

	/**
	 * Removes the specified object from this counter and returns the current count before object
	 * removal. Returns DOES_NOT_EXIST f the object was not contained.
	 * 
	 * @param e
	 *            the object to remove
	 * @return the current count before object removal, or DOES_NOT_EXIST f the object was not
	 *         contained
	 */
	public int remove(E e)
	{
		Integer i = map.remove(e);
		if (i == null)
		{
			return DOES_NOT_EXIST;
		}
		else
		{
			return i;
		}
	}

	/**
	 * Returns the hash code for this CountingSet. The hash code is equal to the hash code of the
	 * underlying Map.
	 */
	public int hashCode()
	{
		return map.hashCode();
	}

	/**
	 * Determines whether this CountingSet equals the specified object. Two instances of
	 * CountingSet are equivalent if they contain the exact same set of objects with the exact
	 * same counts for each. This is equivalent to the definition of Map.equals.
	 */
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (!(obj instanceof CountingSet))
			return false;
		CountingSet other = (CountingSet)obj;

		return map.equals(other.map);
	}

	/**
	 * Returns a String representation of this CountingSet equivalent to the representation of the
	 * underlying Map.
	 */
	public String toString()
	{
		return map.toString();
	}

	/**
	 * Returns a Set of all the objects that exist in this counter. The set is backed by this
	 * counter, so changes to the counter are reflected in the set, and vice-versa. If the map is
	 * modified while an iteration over the set is in progress (except through the iterator's own
	 * remove operation), the results of the iteration are undefined. The set supports element
	 * removal, which removes the corresponding mapping from the map, via the Iterator.remove,
	 * Set.remove, removeAll, retainAll, and clear operations. It does not support the add or addAll
	 * operations.
	 * 
	 * @return a Set of all the objects that exist in this counter
	 */
	public Set<E> objectSet()
	{
		return map.keySet();
	}

	/**
	 * Returns a Set of all object-count mappings in this counter. The set is backed by this
	 * counter, so changes to the counter are reflected in the set, and vice-versa. If the map is
	 * modified while an iteration over the set is in progress (except through the iterator's own
	 * remove operation, or through the setValue operation on a map entry returned by the iterator)
	 * the results of the iteration are undefined. The set supports element removal, which removes
	 * the corresponding mapping from the map, via the Iterator.remove, Set.remove, removeAll,
	 * retainAll and clear operations. It does not support the add or addAll operations.
	 * 
	 * @return a Set of all object-count mappings in this counter
	 */
	public Set<Entry<E, Integer>> countSet()
	{
		return map.entrySet();
	}

	/**
	 * Clears all objects and counts from this counter.
	 */
	public void clear()
	{
		map.clear();
	}

	/**
	 * Determines whether this counter contains a count for the specified object
	 * 
	 * @param e
	 *            the object whose presence in this counter is to be tested
	 * @return true if this counter contains the specified object
	 */
	public boolean contains(Object o)
	{
		return map.containsKey(o);
	}

	/**
	 * Determines whether this counter contains any objects
	 * 
	 * @return true if and only if this counter contains no objects
	 */
	public boolean isEmpty()
	{
		return map.isEmpty();
	}

	/**
	 * Returns the number of objects in this counter.
	 * 
	 * @return the number of objects in this counter
	 */
	public int size()
	{
		return map.size();
	}

	/**
	 * Returns the total sum of all the counts in this counter.
	 * 
	 * @return the total sum of all the counts in this counter
	 */
	public int sum()
	{
		int sum = 0;
		for (Entry<E, Integer> entry : map.entrySet())
		{
			sum += entry.getValue();
		}

		return sum;
	}

	/**
	 * Resets the counts of all objects in this counter to 1. Equivalent to calling resetAll(1).
	 */
	public void resetAll()
	{
		resetAll(1);
	}

	/**
	 * Resets the counts of all objects in this counter to the specified value
	 * 
	 * @param value
	 *            the value to which to set all counts
	 */
	public void resetAll(int value)
	{
		for (E e : map.keySet())
		{
			setCount(e, value);
		}
	}

	/**
	 * Increments the count of each element in the specified array.
	 * If the array contains multiple instances of the same object,
	 * then the count for that object will be incremented once for
	 * each occurrence.
	 * 
	 * @param arr the objects to increment
	 */
	public void incrementAll(E[] arr)
	{
		for (E e : arr)
		{
			increment(e);
		}
	}

	/**
	 * Increments the count of each element in the specified array
	 * exactly once.  If the array contains multiple instances of the same object,
	 * then all occurrences after the first are ignored and the count
	 * will increase only once.
	 * 
	 * @param arr the objects to increment
	 */
	public void incrementAllOnce(E[] arr)
	{
		for (E e : new HashSet<E>(Arrays.asList(arr)))
		{
			increment(e);
		}
	}

	/**
	 * Increments the count of each element in the specified Collection.
	 * If the Collection contains multiple instances of the same object,
	 * then the count for that object will be incremented once for
	 * each occurrence.
	 * 
	 * @param c the objects to increment
	 */
	public void incrementAll(Collection<? extends E> c)
	{
		for (E e : c)
		{
			increment(e);
		}
	}

	/**
	 * Increments the count of each element in the specified Collection
	 * exactly once.  If the Collection contains multiple instances of the same object,
	 * then all occurrences after the first are ignored and the count
	 * will increase only once.
	 * 
	 * @param c the objects to increment
	 */
	public void incrementAllOnce(Collection<? extends E> c)
	{
		if (c instanceof Set)
		{
			for (E e : c)
			{
				increment(e);
			}
		}
		else
		{
			for (E e : new HashSet<E>(c))
			{
				increment(e);
			}
		}
	}
}
